This weekend I have been playing with Nashorn, the new JavaScript engine coming in Java 8.
As an exercise I implemented a JUnit runner for JavaScript unit tests using Nashorn. Others have implemented similar wrappers, we even have one at work. None of the ones I have found do everything I want, and it was a fun project.
Because the JavaScript tests are JUnit tests they “Just work” with existing JUnit tools like Eclipse and as part of your build with ant/maven. The Eclipse UI shows every test function and a useful error trace (Line numbers only work with Nashorn).
There are also lots of reasons you wouldn’t want to do this – your tests have to work in a very Java-y way, and you miss out on great features of JavaScript testing tools. There’s also no DOM, so you may end up having to stub a lot if you are testing code that interacts with the DOM. This can be a good thing and encourage you not to couple code to the DOM.
Here’s what a test file looks like.
tests({ thisTestShouldPass : function() { console.log("One == One"); assert.assertEquals("One","One"); }, thisTestShouldFail : function() { console.log("Running a failing test"); assert.fail(); }, testAnEqualityFail : function() { console.log("Running an equality fail test"); assert.assertEquals("One", "Two"); }, objectEquality : function() { var a = { foo: 'bar', bar: 'baz' }; var b = a; assert.assertEquals(a, b); }, integerComparison : function() { jsAssert.assertIntegerEquals(4, 4); }, failingIntegerComparison : function() { jsAssert.assertIntegerEquals(4, 5); } }); |
You can easily extend the available test tools using either JavaScript or Java. In order to show the failure reason in JUnit tools you just need to ensure you throw a java AssertionError at some point.
The tests themselves are executed from Java by returning a list of Runnables from JavaScript.
var tests = function(testObject) { var testCases = new java.util.ArrayList(); for (var name in testObject) { if (testObject.hasOwnProperty(name)) { testCases.add(new TestCase(name,testObject[name])); } } return testCases; }; |
Where TestCase is a Java class with a constructor like:
public TestCase(String name, Runnable testCase) { |
Nashorn/Rhino will both convert a JavaScript function to a Runnable automatically :)
On the Java side we just create a Test Suite that lists the JavaScript files containing our tests, and tell JUnit we want to run it with a custom Runner.
@Tests({ "ExampleTestOne.js", "ExampleTestTwo.js", "TestFileUnderTest.js" }) @RunWith(JSRunner.class) public class ExampleTestSuite { } |
Our Runner has to create a heirarchy of JUnit Descriptions; Suite -> JS Test File -> JS Test Function
The Runner starts up a Nashorn or Rhino script engine, evaluates the JavaScript files to get a set of TestCases to run, and then executes them.
ScriptEngineManager factory = new ScriptEngineManager(); ScriptEngine nashorn = factory.getEngineByName("nashorn"); if (nashorn != null) return nashorn; // Load Rhino if no nashorn. |
You can quickly implement stubbing that also integrates with your Java JUnit tools.
Here’s the test code from the above screenshot.
load("src/main/java/uk/co/benjiweber/junitjs/examples/FileUnderTest.js"); var stub = newStub(); underTest.collaborator = stub; tests({ doesSomethingImportant_ThisTestShouldFail: function() { underTest.doesSomethingImportant(); stub.assertCalled({ name: 'importantFunction', args: ['wrong', 'args'] }); }, doesSomethingImportant_ShouldDoSomethingImportant: function() { underTest.doesSomethingImportant(); stub.assertCalled({ name: 'importantFunction', args: ['hello', 'world'] }); } }); |
To implement the stub you can use __noSuchMethod__ to capture interactions and store them for later assertions.
var newStub = function() { return { called: [], __noSuchMethod__: function(name, arg0, arg1, arg2, arg3, arg4, arg5) { var desc = { name: name, args: [] }; var rhino = arg0.length && typeof arg1 == "undefined"; var args = rhino ? arg0 : arguments; for (var i = rhino ? 0 : 1; i < args.length; i++){ if (typeof args[i] == "undefined") continue; desc.args.push(args[i]); } this.called.push(desc); }, assertCalled: function(description) { var fnDescToString = function(desc) { return desc.name + "("+ desc.args.join(",") +")"; }; if (this.called.length < 1) assert.fail('No functions called, expected: ' + fnDescToString(description)); for (var i = 0; i < this.called.length; i++) { var fn = this.called[i]; if (fn.name == description.name) { if (description.args.length != fn.args.length) continue; for (var j = 0; j < description.args.length; j++) { if (fn.args[j] == description.args[j]) return; } } } assert.fail('No matching functions called. expected: ' + '<' + fnDescToString(description) + ")>" + ' but had ' + '<' + this.called.map(fnDescToString).join("|") + '>' ); } }; }; |
It is backwards compatible with Rhino (JavaScript Scripting engine in current and old versions of Java). Most things seem just as possible in Rhino, but it’s easier to work with Nashorn due to its meaningful error messages.
You can also run using Nashorn on Java7 using a backport and adding nashorn to the bootclasspath with -Xbootclasspath/a:$NASHORN_HOME/dist/nashorn.jar