Skip to content

Instantly share code, notes, and snippets.

@mbilokonsky
Created May 8, 2015 17:10
Show Gist options
  • Save mbilokonsky/d4e578b5a1c6628ba66b to your computer and use it in GitHub Desktop.
Save mbilokonsky/d4e578b5a1c6628ba66b to your computer and use it in GitHub Desktop.
This should work, right?

Y'all I've been thinking about something. Javascript is great, right? But writing testable javascript is kinda hard. On the one hand, you want to close over your functions so that they don't leak all over your global namespace. On the other hand, once you've done that you can't test any of the functions you've closed over. This is a huge problem!

var computedValue = (function foo(injectedArgument) {
  var myLocalVar = 123;
  
  function someSiblingFunction() {
    console.log("hey this was called, cool.");
  }
  
  function myInnerFunction(x, y) {
    var output = x + y + inferred.injectedArgument - inferred.myLocalVar * inferred.someOtherVarThatCameFromWhoKnowsWhere;
    someSiblingFunction()
    myLocalVar = output;
    return output;
  }

  return myInnerFunction(5, 10);
})(someVariable);

But! When we write tests, especially unit tests, we don't really care about the context in which these functions exist - really, we just want the string of text that represents the function, so that we can run tests against it in isolation. Sometimes (frequently, depending on your coding style) the function may refer to things other than its inputs - calls to things in its scope that were defined somewhere up above ('private' variables closed into the same scope as your function, but as siblings to that function, for instance. Or even just global variables. Or anywhere in between).

So given the snippet above, what if I really want to write some unit tests for myInnerFunction? Right now there's no way to do that, because the javascript interpreter seals that off.

But what if we were to have a completely different javascript interpreter? One whose job was not to take source code as input and create a functioning runtime as output, but rather one whose job it was to detect every function in the codebase and extract it into an isolated context for testing. This shouldn't be THAT hard, right?

We would parse mySource.js and when it got to the closure above it would find function myInnerFunction(x, y){...} and extract it out. It could then go one step further and look at every variable within the scope of that functions internal context and extract them out. So given the source code above, it could generate a new file that looks something like this:

(Note, this is not a proposal for what actual output would look like - I'm just trying to communicate the idea. There'll be some way to cleverly wrap everything up and generate clean tests)

// generated test for function myInnerFunction
// inferred external state - we could maybe even infer sample values, or at least types?
// you can set default values here for tests below, or set them explicitly in the tests.
var injectedArgument = undefined;
var myLocalVar = undefined;
var someOtherVarThatCameFromWhoKnowsWhere = undefined;
var someSiblingFunction = jasmine.createSpy('someSiblingFunction')

function myInnerFunction(x, y) {
  var output = x + y + inferred.injectedArgument - inferred.myLocalVar * inferred.someOtherVarThatCameFromWhoKnowsWhere;
  someSiblingFunction()
  myLocalVar = output;
  return output;
}

// implement your tests here
function sampleTest() {
  injectedArgument = 1;
  myLocalVar = 1;
  someOtherVarThatCameFromWhoKnowsWhere = 1;
  var output = myInnerFunction(5, 10);
  expect(output).toBe(15);
  expect(myLocalVar).toBe(output);
  expect(someSiblingFunction).toHaveBeenCalled();
}

This works because we recognize a few things:

  1. A pure function is super easy to test, because you just have to control for explicit inputs and test explicit outputs.
  2. All functions can be modeled as pure functions if you treat scope as an explicit input and side effects as explicit output.
  3. Reads, writes and calls to values in the scope can be algorithmically identified and extracted and isolated.
  4. At the unit test level, we can then treat relevant items in the scope as controlled contexts

Does this make sense? Am I missing something obvious? Because I would absolutely love to add a step to my build which extracts every function in my code and generates test stubs for me to play with. How easy would this be to write?

@TravisTheTechie
Copy link

  1. Moar clojures. If you break it apart into smaller bits you can test your smaller bits.
  2. Side effects are the hater or all haters. If you shrink the side effects to the smallest bits, and then write code that you can compose around that, I've found my life to be simpler.
  3. You can always expose internals on the prototype of your function. Weird, but it works.

@mbilokonsky
Copy link
Author

Major tradeoff I see is brittleness of tests generated in this fashion. The generated tests are explicitly testing your implementation - if you tweak your implementation at all you invalidate the test. But I still think there's huge utility here.

@jimkang
Copy link

jimkang commented May 8, 2015

While I do think this would be cool, I think you could avoid a build step (and AST parsing and trouble) by writing that outer function as a constructor that returns (or export) objects instead of returning (or exporting) a function. I do this a lot. Here's an example.: There, not every function that is returned in the object is used, but they can be exported for testing.

If we were to apply that your example, it would look like this:

function createFoo(injectedArgument) {
  var myLocalVar = 123;

  function someSiblingFunction() {
    console.log("hey this was called, cool.");
  }

  function myInnerFunction(x, y) {
    var output = x + y + inferred.injectedArgument - inferred.myLocalVar * inferred.someOtherVarThatCameFromWhoKnowsWhere;
    someSiblingFunction()
    myLocalVar = output;
    return output;
  }

  return {
    myInnerFunction: myInnerFunction
  };
}

var foo = createFoo(someVariable);
foo.myInnerFunction(5, 10);

(It could still be an IIFE – I just unrolled it for clarity.) Then, if you wanted to later test someSiblingFunction, you could do so by simply adding it to the returned object:

return {
  myInnerFunction: myInnerFunction,
  someSiblingFunction: someSiblingFunction
};

@mbilokonsky
Copy link
Author

Ah, I totally see what you're saying and that makes a lot of sense - except now I'm littering my production code with with all of these handles to internal implementation, and my encapsulation has been violated. Additionally, this means that it's not a general purpose solution - I have to write my code in a specific way to be able to do it.

Personally, I do write my code more or less this way - but what if I inherit, say, thousands of lines of legacy javascript with no test coverage? I want to refactor, but first I need to add tests to make sure I'm not breaking anything when I do. But the code wasn't written in a testable way - now I have a problem.

This isn't an esoteric concern, it happens kind of regularly. So, sure, I could slowly start rewriting and adding tests - but then I'm refactoring before testing, so I don't actually have any way to make sure that the things I care about aren't being changed.

So, for this idea, yeah it's going to take a bit more work up front, but the outcome is the ability to generate comprehensive tests for every function in a codebase.

(As an aside: once this works, I can also do crazy shit like extract functions into their own remotely addressable actors and make what used to be a single monolithic JS app into a big distributed app... but that's maybe a can of worms I'll open down the line. ;))

@jimkang
Copy link

jimkang commented May 8, 2015

Ah, I can't address the legacy code case (in which I think maybe you'd want to write integration or functional tests before refactoring, rather than unit tests which may end up testing units you'll throw away), but as for encapsulation in new code, you can always pass a test flag to your constructor which will let it decide what to export – everything for tests, just certain functions for production.

I definitely think it would be neat if your alternate JS engine were implemented (or text preprocessor, perhaps, to make things easier) and am not trying to dissuade you from doing it! Just noting ways to make stuff testable easily with current JS engines.

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