Skip to content

Instantly share code, notes, and snippets.

@unscriptable
Last active February 12, 2024 00:37
Show Gist options
  • Save unscriptable/5144589 to your computer and use it in GitHub Desktop.
Save unscriptable/5144589 to your computer and use it in GitHub Desktop.
example of possible curl/tdd/isolate

curl/tdd/runner is a bit complicated atm. Just thinking of something that might be a bit simpler:

curl(['curl/tdd/isolate'], function (isolate) {

	// inject AMD `require` and `define`, as well as a "done" callback.
	// the test function is guaranteed to run in isolation and all modules 
	// are undefined afterward.
	isolate(function test (require, define, done) {

		// define mocks:
		define('pkg/mod1', { foo: 42 });
		define('pkg/mod2', ['pkg/dep1'], function (dep1) {
			return {
				bar: function (val) { return String(dep1(val)); }
			}
		});

		// fetch the module to test and any unmocked modules
		require(['pkg/unit/to/test'], function (unitToTest) {
			// tests go here
			assert.equals('a string', unitToTest.method('a string'));
			unitToTest.asyncThing(function (val) {
				// more tests
				assert.true(val);
				done();
			})
		});

		// hmmm. anything here will exec before require callback and may
		// cause confusion?

	});

});

Here's another possible API that removes the uncertainty of code around the async require:

curl(['curl/tdd/isolate'], function (isolate) {

	// the test function is run in isolation and the mocker function is run
	// immediately prior.  all modules are undefined afterward.
	isolate(
		['pkg/unit/to/test', 'other/unmocked/thing'],
		function mocker (define) {
			// define mocks:
			define('pkg/mod1', { foo: 42 });
			define('pkg/mod2', ['pkg/dep1'], function (dep1) {
				return {
					bar: function (val) { return String(dep1(val)); }
				}
			});
		},
		function test (unitToTest, otherThing, /* yuk: extra param */ done) {
			// tests go here
			assert.equals('a string', unitToTest.method('a string'));
			unitToTest.asyncThing(function (val) {
				assert.true(val);
				done();
			})
		}
	);

});

It should be pretty easy to make it configurable:

// example config to use curl/tdd/isolate with requirejs
isolate.config({
	require: requirejs,
	define: define, // redundant
	undefine: requirejs.undefine
});
// setup and teardown ???
isolate.config({
	setup: mySetupFunction, // runs before each mocker function
	teardown: myteardownFunction // runs after each test
});

Since js is single-threaded, we could obtain a done function inside the test:

function test (unitToTest, otherThing) {
	var done = isolate.waitFor('my test'); // get a named "done" function
	// tests go here
	assert.equals('a string', unitToTest.method('a string'));
	unitToTest.asyncThing(function (val) {
		assert.true(val);
		done();
	})
}
@briancavalier
Copy link

Hmmm, yeah, I like things about each one, but I think I like the 2nd better. What if we reverse the test() and mocker() functions, making the test the most visible and obvious thing, and making it easy for someone to test without supplying a mocker?

Just brainstorming out loud here:

curl(function (require) {
    // Yuck, 2 require()s!
    var isolate = require('curl/tdd/isolate');

    // the test function is run in isolation and the mocker function is run
    // immediately prior.  all modules are undefined afterward.
    isolate(
        function test (require) {
            // tests go here
            var unitToTest = require('...');
            var otherThing = require('...');

            assert.equals('a string', unitToTest.method('a string'));
            unitToTest.asyncThing(function (val) {
                assert.true(val);
                require.done();
            })
        },
        function mocker (define) {
            // define mocks, as above ...
        }
    );
});

I don't like that there are 2 requires in that example (just figured I'd try it--it doesn't have to be written that way), but hanging done() off of the inner require seems potentially interesting.

How will the module ids given to define() in mocker play with AMD path/package mappings? Will path/package mappings even be used in tdd at all? Will users need a 2nd set of "override" path/package mappings?

@briancavalier
Copy link

I think I was insane above trying to use an outer require like that. Let's try that again:

curl(['curl/tdd/isolate'], function (isolate) {

    // the test function is run in isolation and the mocker function is run
    // immediately prior.  all modules are undefined afterward.
    isolate(
        function test (require) {
            // tests go here
            var unitToTest = require('...');
            var otherThing = require('...');

            assert.equals('a string', unitToTest.method('a string'));
            unitToTest.asyncThing(function (val) {
                assert.true(val);
                require.done();
            })
        },
        function mocker (define) {
            // define mocks, as above ...
        }
    );
});

@unscriptable
Copy link
Author

Even though I complain heavily every time somebody proposes a new property on require, I actually like this one. :)

It's a special kind of require here.

Do you think ppl will like putting mocks below? It feels backwards to a sync mind.

@briancavalier
Copy link

My "put the most important stuff first" brain wants the mocks to be at the bottom, but yeah, I can see that at least chronologically, they have to exist first, so people may prefer to see them at the top. I'm fine with either order, ultimately, as long as it's easy to leave out the mocker function entirely if I don't need mocks.

@unscriptable
Copy link
Author

I like the look of the commonjs-style sync require() version. I think I could still get that to work with other AMD loaders, too. Behind the scenes, it'd have to do something like this to invoke the "AMD-wrapped CommonJS" rules in a cross-loader way:

define('some-unique-name' + counter++, isolatedTestFunc);

@scothis
Copy link

scothis commented Mar 12, 2013

I see the benefits for pure AMD modules, but for UMD modules, how well is this going to work on Node? My test case should be just as portable as my module. In that case I'd want to use node's native require instead of curl's.

@scothis
Copy link

scothis commented Mar 12, 2013

Do you need a done function? If the context follows the require/define functions, any modules loaded with that require/define should use that context. Each invocation of the test function should get a fresh context. done() looks like a hack to avoid contextualizing require/define.

Test runners, buster in particular, want to build up the full test suite context before starting to invoke the tests. That means curl would need to provide all of the environments before any of the actual tests start to execute. This should just work with normal js closure scopes, but it's good to keep in mind.

@unscriptable
Copy link
Author

Thanks for the heads-up about CJS and node environs. It seems the isolate module could be made to work in those environs, too. I imagine it would just mock the define(). There's not as much need to deal with async -- at least when require()ing modules.

My initial thoughts were that the only way to truly isolate the context was via time slicing, but since we're also intercepting the define(), the contexts may be able to execute in parallel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment