Monday 30 May 2022

CFML: Implementing an ObservableScopeAdapter using the Adapter Pattern, Decorator Pattern and Observer Pattern

G'day:

In my last article (A super-quick Observer Pattern implementation in CFML, and I skip TDD. Or do I?), I did what it suggests: I created a very simple observer pattern implementation in CFML.

Why? Well: to copy and paste from that article:

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.

I wanted to keep the article brief-ish and on-point, so just focused on one part of it. In this article I'll put that observer service to use in an application scope adapter.

Firstly I need a scope adapter. The interface to this will be the same for any scope, so I'm gonna keep the name generic.

Well: firstly I need some tests to clearly define how the thing should work:

describe("Tests for set method", () => {
    it("sets a key's value in the scope", () => {
        var scopeAdapter = new ScopeAdapter(local)

        scopeAdapter.set("testVariable", "test value")

        expect(local).toHaveKey("testVariable")
        expect(local.testVariable).toBe("test value")
    })
})

describe("Tests for get method", () => {
    it("gets a value from the scope by its key", () => {
        var scopeAdapter = new ScopeAdapter(local)
        var testVariable = "test value"

        var result = scopeAdapter.get("testVariable")

        expect(result).toBe("test value")
    })
})

And a very simple implementation:

component {

    public function init(required struct scope) {
        variables.scope = arguments.scope
    }

    public void function set(required string key, required any value) {
        variables.scope[key] = value
    }

    public any function get(required string key) {
        return variables.scope[key]
    }

}

And that works. In real life before this was production-ready we'd probably want some error-handling around trying to get values that aren't set, and poss something to handle overwriting values etc. But for my purposes here: this will do.

When I first planned the code for this article in my head, the next step was gonna be to initialise the ScopeAdapter with an ObserverService object, and have it fire off some events on set. Then it occurred to me that this is breaking the Single Responsibility Principle a bit. It's not for the general ScopeAdapter to be doing anything other than being an adapter to a scope. So what I'm going to do is to use the Decorator Pattern: I'm going to create a decorator for a ScopeAdapter that is an ObservableScopeAdapter.

I need to back up slightly and do a slight refactor. If I'm using a decorator, then I need to be able to code to an interface, not to an implementation. I can't be having code that takes specifically a cfml.forBlog.applicationScopeAdapter.ScopeAdapter object; I need it to take an implementation of a cfml.forBlog.applicationScopeAdapter.ScopeAdapter interface. This is because when using a decorator, obviously it's no longer the same concrete implementation class. EG: if I have SomeService class, and it takes as one of its constructor parameters a ScopeAdapter object, I can't then initialise it with an ObservableScopeAdapter. One might think this is solved by making ObservableScopeAdapter simply extend ScopeAdapter. This would work, but it's a bit of an anti-pattern, and I discuss this in "Decorator Pattern vs simple inheritance". Instead we provide an interface for both ObservableScopeAdapter and the current ScopeAdapter (which will need to be renamed) to implement, and we make our SomeService take one of those as its argument.

I'm going to rename ScopeAdapter to be GeneralScopeAdapter, and create an interface that it implements called ScopeAdapter. Yay for tests, as I am refactoring here, not just "changing shit". The only new code here is the interface:

interface {
    public void function set(required string key, required any value);
    public any function get(required string key);
}

Now we want our decorator. It will do two things:

  • Hand off any calls to methods in the ScopeAdapter interface to a full implementation of a ScopeAdapter it has been configured with
  • When set is called, it triggers an event that other code can have subscribed to.

"Hang on", you might say "isn't it doing two things, which is still a violation of the SRP?". Not really. It's only implementing the event trigger part. It's delegating the scope-adapting to its dependent GeneralScopeAdapter. Indeed it's not even implementing the event-triggering side of things. It's delegating that to the ObserverService. So the one thing the decorator is implementing is "making the scope access observable". Make sense?

Anyway here are some tests to set our expectations:

import cfml.forBlog.applicationScopeAdapter.GeneralScopeAdapter
import cfml.forBlog.applicationScopeAdapter.ObservableScopeAdapter
import cfml.forBlog.observerService.SimpleObserverService

component extends=Testbox.system.BaseSpec {
    function run() {
        describe("Tests for ObservableScopeAdapter", () => {
            describe("Tests for set method", () => {
                it("functions as a ScopeAdapter when setting a key's value in the scope", () => {
                    // same implementation as above
                })

                it("triggers an event when set is called, which receives the key/value of the set call", () => {
                    var eventLog = []
                    var localScopeAdapter = new GeneralScopeAdapter(local)
                    var observerService = new SimpleObserverService()
                    var observableScopeAdapter = new ObservableScopeAdapter(localScopeAdapter, observerService)

                    observerService.on("scope.set", (event) => {
                        eventLog.append(event)
                    })

                    observableScopeAdapter.set("testVariable", "test value")

                    expect(eventLog).toHaveLength(1)
                    expect(eventLog[1]).toBe({
                        name = "scope.set",
                        data = javaCast("null", ""),
                        detail = {
                            key = "testVariable",
                            value = "test value"
                        }
                    })
                })
            })

            describe("Tests for get method", () => {
                it("functions as a ScopeAdapter when getting a value from the scope by its key", () => {
                    // same implementation as above
                })
            })
        })
    }
}

Notes:

  • I've changed the name of the ObserverService to be SimpleObserverService because I've extracted an interface for that too, and now that is called ObserverService. I didn't need to do this, but I figured it was more balanced, and "one should program to an interface", etc
  • Where I say same implementation as above, it's literally the same implementation as the earlier tests, because I want to test that the decorated implementation still behaves the same.
  • data is data passed along with the on call, which I am not using here.
  • I had actually neglected to add the feature of trigger to accept details (ie: data) at trigger-time in my initial implementation of BasicObserverService! So I had to add that functionality in. See below.

There's not much to the implementation:

import cfml.forBlog.applicationScopeAdapter.GeneralScopeAdapter
import cfml.forBlog.observerService.ObserverService


component implements=ScopeAdapter {

    public function init(required GeneralScopeAdapter scopeAdapter, ObserverService observerService) {
        variables.scopeAdapter = arguments.scopeAdapter
        variables.observerService = arguments.observerService
    }

    public void function set(required string key, required any value) {
        variables.observerService.trigger("scope.set", arguments)
        variables.scopeAdapter.set(argumentCollection=arguments)
    }

    public any function get(required string key) {
        return variables.scopeAdapter.get(argumentCollection=arguments)
    }

}
  • It implements the same interface as GeneralScopeAdapter, so it can be used anywhere that one of those is required.
  • It's initialised with an underlying GeneralScopeAdapter that does all the scope-adapting, as well as an ObserverService which does all the event handling.
  • get simply proxies to the underlying GeneralScopeAdapter
  • set does that too, but not before triggering an event that it's been called.

As I touched on above, I needed to add a feature to SimpleObservrService so that it passed detail data when triggering an event. Here's the test:

it("passes any extra details as part of the event", () => {
    var observerService = new SimpleObserverService()

    var eventResults = []

    observerService.on("testEvent", (event) => {
        eventResults.append([
            message = "test event handler 1",
            event = event
        ])
    }, {key="value set in handler"})

    observerService.trigger("testEvent", {
        key = "value set by trigger"
    })
    expect(eventResults).toBe([[
        message = "test event handler 1",
        event = {
            name = "testEvent",
            data = {key = "value set in handler"},
            detail = {key = "value set by trigger"}
        }
    ]])
})

That's self-explanatory I think.

And implementation:

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

This broke one other test, but in an expected way because it was testing what came back in the event struct, focusing on testing the data part, but it did not expect the new (empty) detail part of the data. All the other tests continued to pass, so I know this change has no unexpected side-effects.


That's it, all working. But how does one wire this into one's app? Well. Any place one currently accesses the application scope (remember the initial requirement was to trigger events when there were changes to the application scope) directly in the app needs to be swapped out for an instance of ObservableScopeAdapter that is adapting the application scope. This can be done easily enough with a DI framework: just inject it into whichever objects formerly accessed the application scope directly, and then use it. Secondly: wherever it is relevant to react to the "scope.set" event: pass in an instance of ObserverService, and attach an event handler to the "scope.set" event that does whatever one needs to. Obviously (?) it needs to be the same instance of ObserverService that the ObservableScopeAdapter is using. Again: super easy with your DI framework.

I realise this section is a bit like this:

Possibly © 9gag.com - I am using it without permission. If you are the copyright owner and wish me to remove this, please let me know.

But it's not really in scope here to explain how dependency injection works. You should already know.


The original question is begging slightly though. It presupposed the situation where there's application-scope access all over the place in an application. In a well-designed app, there simply shouldn't be. Everything ought to be managed by a DI container, and no actual objects ought to know what scope they are in, or care what scope their dependency objects are in. And data values that need to persist for the life of the application should just be in objects that persist for the life of the application, so this should not directly involve the application scope at all.

The same applies for any scope in your CFML code. The only scopes that should generally be relevant to your code are:

  • The variables scope of the CFC the code is in; reflecting the state of the object, or its dependencies, etc
  • The arguments scope of any argument values being passed into your function.
  • The local scope within your function.
  • One is possibly tied to some thread-specific scopes if using cfthread

That's it. If yer accessing any scopes other than those, you should be questioning the design of your application, I think.

The code for this lot is in Github @ /forBlog/applicationScopeAdapter (and its tests) and /forBlog/observerService (and its tests).

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

Wednesday 18 May 2022

CFML: Filler article with code but no TDD at all

G'day:

I'm still working on the TinyTestFramework though: some things don't change.

My test file for this work, which is the framework and all its tests in one is getting a bit weighty: >1200 LOC, and I'm fiding it difficult to navigate about the place. Especially as I'm using trycf.com as my dev environment :-).

Using the technique I recently documented to execute code on trycf.com remotely ("Running CFML code on trycf.com via a remote HTTP request"), I've split-out the framework and the tests into different gists, and wrote a wee calling-harness to run them all… and for completeness on both ColdFusion 2021 and Lucee 5.

This way I can just have the new tests I am working on in front of me, and have all the rest of the tests running in a different trycf.com window.

It's not doing anything complicated: just some loops and some HTTP requests to get the code from GitHub, and send it to trycf.com; and then outputing the response:

<cfscript>
cfhttp(
    method = "get",
    url = "https://gist.githubusercontent.com/adamcameron/816ce84fd991c2682df612dbaf1cad11/raw/tinyTestFramework.cfm",
    result = "frameworkCodeResponse",
    throwOnError = true
    
);
frameworkCode = frameworkCodeResponse.fileContent;

testSuites = [
    "Tests for misc functions that don't fit another category" = {
        guid = "332e3cda31fa933cfe3a783be07bc59e",
        file = "ttfOtherFunctionsTest.cfm"
    },
    "Tests for matcher functions" = {
        guid = "b006d2c420dd4cbe369b6c809c15ea83",
        file = "ttfMatcherFunctionsTest.cfm"
    },
    "Tests for lifecycle functions" = {
        guid = "f93c3e12885f2913bfc8351ba1ed8911",
        file = "ttfLifeCycleFunctionsTest.cfm"
    }
]

engineUrls = [
    "ColdFusion 2021" = "https://acf14-sbx.trycf.com/getremote.cfm",
    "Lucee 5" = "https://lucee5-sbx.trycf.com/lucee5/getremote.cfm"
]    
    
writeOutput('<div class="tinyTest">');
testSuites.each((label, suite) => {
    writeOutput("<h3>#label#</h3>");
    cfhttp(
        method = "get",
        url = "https://gist.githubusercontent.com/adamcameron/#suite.guid#/raw/#suite.file#",
        result = "testCodeResponse",
        throwOnError = true
    );
    
    testCode = testCodeResponse.fileContent;
    
    engineUrls.each((engine, engineUrl) => {
        cfhttp(method="post", url=engineUrl, result="testRunResponse") {
            cfhttpparam(type="formField", name="setupcode", value=frameworkCode);
            cfhttpparam(type="formField", name="code", value=testCode);
            cfhttpparam(type="formField", name="asserts", value="");
        }
        writeOutput("<h4>#engine#</h4>");
        writeOutput(testRunResponse.fileContent)
        writeOutput("<hr>")
    })
})
writeOutput("</div>")
</cfscript>

(Gist on Github)

And the output's like this:

That'll do for the evening, I think.

Righto.

--
Adam

Tuesday 17 May 2022

Why I'm doing this TinyTestFramework exercise

G'day:

I have listened to both Modernize or Die® and Working Code Podcast (the specific episode of the latter is not public yet: I get it early as I'm a patreon… which you should be too: Patreon: Working Code is creating podcasts) this evening, and I appreciate them both mentioning my series on TinyTestFramework, but they both kinda got my motivations for doing it wrong.

I'm not doing it for these reasons. I mean… I am doing it, yes, obviously. But these are not my motivations:

  • To work out how to write a testing framework. This is not really that interesting to me. How to do it within a single expression (the whole thing is one struct literal)? Yes, interested in that. Def.
  • Because I think TestBox is doing anything wrong. It could not be further from the truth: I think TestBox is one of the best CFML projects out there. I am using it as inspiration, as I want code written in TinyTestFramework to be lift-and-shift-able into TestBox.

The reason why I am doing that project is n-fold:

  • I want to enable CFMLers to be able to run tests on trycf.com. Why:
    • I personally want to be able to present example code as tests to make it clear what I am trying to demonstrate (bug, idea, concept, help);
    • I want to encourage other CFMLers to think about asking question via TDD. Think through what they're asking, and think through how they derive their repro cases, showing us what they expect as an outcome. In a portable way.
  • As a practical exercise in TDDing some real-world code. I mean granted it's a slightly contrived situation - developing a test framework for trycf.com - but it's a real world project I am working on. And I am TDDing the whole lot.
  • For me personally: implementing non-trivial code in a restricted environment, and still come up with decent code. This project is a challenge for me.
  • A - largely in-vain, I suspect - attempt at intriguing some of the testing-nae-saying fuckwits out there to actually look at testing. Yeah good luck with that, Cameron.

Well then. That was cathartic.

Righto.

--
Adam

If yer a CFML dev, you should consider financially supporting trycf.com

G'day:

I'll keep this on-point today.

If you are a CFML developer, you will be aware and likely use trycf.com. Whenever I have an issue with some CFML that needs to be demonstrated to someone else; eg: I'm asking for help on Slack or Stack Overflow, or demonstrating an answer to someone else's question: I create a portable / repeatable repro case on trycf.com. I use it to demonstrate bugs and behavioural differences to Adobe or Lucee when both vendors don't give the same result from the same code. I use it every day.

I believe trycf.com is the handiest resource available to CFML developers.

Until now Abram has done all the work to create and maintain trycf.com at his own expense, both financially and with his own time. I reached out to him a coupla days ago wanting to see if I could help-out financially, and he has set up a Patreon page for this very reason. His blurb is:

And the Patreon link is: https://www.patreon.com/trycf/posts.

If you don't want to do a monthly payment, there's also an option to flick him some dosh on a one off basis: https://buy.stripe.com/28o8y6dzHh1bezucMM

Both those links are on the trycf.com homepage.

Go give him some money. If you're the purse-string holder at a company: consider giving him some money on behalf of your company too, to make yourselves better CFML Community participants.

Cheers.


Whilst I'm talking about money, don't forget you perhaps also ought to be supporting Lucee. I wrote about this before: If your company (or yourself) makes money using Lucee… you should throw them a bone. Consider supporting them as well if you haven't already.

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

Friday 13 May 2022

CFML: adding aroundEach to TinyTestFramework was way easier than I expected

G'day:

I'm still pottering around with my TinyTestFramework. Last night I added beforeEach and afterEach handlers, but then thought about how the hell I could easily implement aroundEach support, and I could only see about 50% of it, so I decided to sleep on it.

After a night's sleep I spent about 30min before work doing a quick spike (read: no tests, just "will this even work?"), and surprisingly it did work. First time. Well except for a coupla typos, but I nailed the logic first time. I'm sorta halfway chuffed by this, sorta halfway worried that even though what I decided would probably work - and it did - I haven't quite got my head around how it works, or even quite what it's doing. So let's blog about that.

This evening after work I ignored my spike, and wrote some tests. This is another TDD lesson: it's OK to do a spike in yer code without tests and stuff to just prove a proof of concept, but then once yer good with that, put it to once side and go back to writing tests. My spike code is in a completely different environment from the environment I'm writing the tests in.

For the tests, I consider a lot of the testing of the hierarchical mechanics of how a describe / it testing framework works has already been tested extensively by the beforeEach tests (see "CFML: Adding beforeEach handlers to my TinyTestFramework. Another exercise in TDD"), and I don't need to restest that. This is the same reason I tested the afterEach stuff quite lightly too ("CFML: for the sake of completeness, here's the afterEach treatment"). In those afterEach tests I just tested the "afterness" of the way it works, which is the only difference in the implementation of afterEach compared to beforeEach. The chief difference being that when there is a hierarchy of afterEach handlers, they are called in the reverse order from equivalent beforeEach handlers.

For aroundEach what I am testing is the "aroundness" of how it works.

Here are the tests:

describe("Tests of aroundEach", () => {
    describe("Tests hierarchical sequencing", () => {
        result = []
        aroundEach((test) => {
            result.append("aroundEach top level before test")
            test()
            result.append("aroundEach top level after test")
        })
        describe("Tests hierarchical sequencing (second level: no aroundEach in this one)", () => {
            describe("Tests hierarchical sequencing (third level)", () => {
                aroundEach((test) => {
                    result.append("aroundEach third level before test")
                    test()
                    result.append("aroundEach third level after test")
                })
                describe("Tests hierarchical sequencing (inner level)", () => {
                    aroundEach((test) => {
                        result.append("aroundEach inner before test")
                        test()
                        result.append("aroundEach inner after test")
                    })
                    
                    it("is the baseline test", ()=> {
                        expect(true).toBeTrue()
                    })

                    it("tests the aroundEach handlers are called in the correct order", ()=> {
                        result.append("tests the aroundEach handlers are called in the correct order")

                        expect(result).toBe([
                            "aroundEach top level before test",
                            "aroundEach third level before test",
                            "aroundEach inner before test",
                            "aroundEach inner after test",
                            "aroundEach third level after test",
                            "aroundEach top level after test",
                            "aroundEach top level before test",
                            "aroundEach third level before test",
                            "aroundEach inner before test",
                            "tests the aroundEach handlers are called in the correct order"
                        ])
                    })
                })
            })
        })
    })

    describe("Tests with beforeEach and afterEach", () => {
        result = []

        afterEach(() => {
            result.append("set by afterEach")
        })

        beforeEach(() => {
            result.append("set by beforeEach")
        })

        aroundEach((test) => {
            result.append("set by aroundEach before test")
            test()
            result.append("set by aroundEach after test")
        })

        it("is the baseline test", ()=> {
            expect(true).toBeTrue()
        })

        it("tests the aroundEach handlers are called in the correct order", ()=> {
            result.append("tests the aroundEach handlers are called in the correct order")

            expect(result).toBe([
                "set by beforeEach",
                "set by aroundEach before test",
                "set by aroundEach after test",
                "set by afterEach",
                "set by beforeEach",
                "set by aroundEach before test",
                "tests the aroundEach handlers are called in the correct order"
            ])
        })
    })
})

So we have:

  • Test how a collection of hierarchical aroundEach handlers work. It looks like a lot of code, but it's just three nested describe blocks each with an aroundEach handler, before finally a test to observe, and a test that does the observation and tests some expectations on same.
  • Test that aroundEach plays nice with beforeEach and afterEach. Again a few lines of code, but pretty simple stuff, and the expectations are pretty clear and straight forward.

Um. OK. So.

The implementation bit is going to be a bit of an anti-climax after that lot. Firstly there's some scaffolding stuff that's obviously needed:

beforeEach = (callback) => {
    tinyTest.contexts.last().beforeEachHandler = callback
},
afterEach = (callback) => {
    tinyTest.contexts.last().afterEachHandler = callback
},
aroundEach = (callback) => {
    tinyTest.contexts.last().aroundEachHandler = callback
},
// ...
beforeEach = tinyTest.beforeEach
afterEach = tinyTest.afterEach
aroundEach = tinyTest.aroundEach

And the implementation is changing this:

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

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

            implementation()

            tinyTest.contexts.reduce((reversedContexts, context) => reversedContexts.prepend(context), []).each((context) => {
                context.keyExists("afterEachHandler") ? context.afterEachHandler() : false
            })

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

To be this:

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

decoratedImplementation()

That's not much code, but it's a wee bit dense, even with putting each step on a separate line. What are we doing:

  • Taking the context collection.
  • Filtering out all the context objects with no aroundEachHandler element. We don't care about those.
  • Flipping the remainder around, as we need to apply them from the one nearest the test to the furthest.
  • Then, starting with the test implementation callback, sequentially pass it to the preceding callback in the hierarchy, if that makes sense. It's kind of a partial-application of each aroundEach callback I guess.
  • Once we've done that, call the returned function.

In effect what I think I am doing is calling a function that calls each aroundEachHandler callback from outermost to innermost, passing it the next handler down as its argument, all the way until the test implementation gets called at the end.

Once that was working, I realised that the filtering step could be applied to the beforeEach and afterEach calls too:

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

Becomes:

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

And equivalently with afterEach:

tinyTest.contexts.reduce((reversedContexts, context) => reversedContexts.prepend(context), []).each((context) => {
    context.keyExists("afterEachHandler") ? context.afterEachHandler() : false
})

Becomes:

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

The usage of ?: didn't sit well with me before, and I like this approach. Of course YMMV.

To copy and paste from last night's article: that's it. I'll add these to the source code:

You know what? I'm quite pleased that the aroundEach handling was so easily solved with just four chained method calls. It seems amazing to me for some reason. I don't mean my code is amazing: I mean the technique is.

Righto.

--
Adam

Thursday 12 May 2022

CFML: for the sake of completeness, here's the afterEach treatment

G'day:

This immediately follows on from "CFML: Adding beforeEach handlers to my TinyTestFramework. Another exercise in TDD".

Having done the beforeEach implementation for my TinyTestFramework, I reckoned afterEach would be super easy: barely an inconvenience. And indeed it was. Took me about 15min, given most of the logic is the same as for beforeEach.

Here are the tests:

describe("Tests of afterEach", () => {
    it("will not break if an afterEach is not specified for a given describe block", () => {
        expect(true).toBeTrue()
    })
    
    describe("Baseline", () => {
        result = []
        afterEach(() => { 
            result.append("set in afterEach")
        })
        
        it("runs after a test (setup)", () => {
            expect(result).toBe([])
        })
        
        it("runs after a test (test)", () => {
            expect(result).toBe(["set in afterEach"])
        })
    })
    
    describe("Works in a hierarchy (top)", () => {
        result = []

        afterEach(() => { 
            result.append("set in afterEach in outer describe")
        })
        
        describe("Works in a hierarchy (middle)", () => {
            afterEach(() => { 
                result.append("set in afterEach in middle describe")
            })
            
            describe("Works in a hierarchy (inner)", () => {
                afterEach(() => { 
                    result.append("set in afterEach in inner describe")
                })
                
                it("runs all afterEach handlers, from innermost to outermost (setup)", () => {
                    expect(result).toBe([])
                })
                
                it("runs all afterEach handlers, from innermost to outermost (test)", () => {
                    expect(result).toBe([
                        "set in afterEach in inner describe",
                        "set in afterEach in middle describe",
                        "set in afterEach in outer describe"
                    ])
                })
            })
        })
    })
    
    describe("Tests with beforeEach as well", () => {
        result = []
        
        afterEach(() => {
            result.append("set by afterEach")
        })

        beforeEach(() => {
            result.append("set by beforeEach")
        })

        it("is the setup test", () => {
            expect(true).toBeTrue()
        })
        it("tests that both beforeEach and afterEach got run", () => {
            result.append("testing that the preceding setup test had its afterEach called")

            expect(result).toBe([
                "set by beforeEach", // setup test
                "set by afterEach", // setup test
                "set by beforeEach", // this test
                "testing that the preceding setup test had its afterEach called"
            ])
        })
    })
})

These are more superficial than the beforeEach ones because most of it is already tested in those tests. I just test that it's called, works in a hierarchy (no reason why it won't given the implemntation requirements, but it's a belt-n-braces sort of test), and works with a beforeEach in play before. One thing to note is that I need to run a stub/control/setup test before my test of afterEach, because obviously it runs after the test's code, so we can't test what it does with a single test. Hopefully you see what I mean there. That's the chief difference.

The implementation is simple:

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

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

            implementation()

            tinyTest.contexts.reduce((reversedContexts, context) => reversedContexts.prepend(context), []).each((context) => {
                context.keyExists("afterEachHandler") ? context.afterEachHandler() : false
            })

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

That is literally it.

The only non-obvious thing is this:

tinyTest.contexts.reduce((reversedContexts, context) => reversedContexts.prepend(context), []).each((context) => {
    context.keyExists("afterEachHandler") ? context.afterEachHandler() : false
})

Where the beforeEach handlers are run from the outermost to the innermost context, the afterEach ones are run in the reverse order.

That's it. I'll add these to the source code:

Righto.

--
Adam

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