Webpack from Nothing

Unit Testing

Unit testing has always been possible in JavaScript, but early test runners required opening a page in a web browser that executed the tests. Because of Node, we now can execute JavaScript on the command-line, without a browser, and this is how we'd like to work, because it's faster and easier to automate (e.g. in a continuous integration systems).

Spoiler: we don't get to work this way.

We can get close, by executing our tests in PhantomJS, a headless version of WebKit.

Ideally, we want the ability to:

There are many JavaScript testing frameworks, though even the definition of what a testing framework is is unclear. We'll start by finding a reasonably complete library that allows us to write tests and assertions.

Jasmine fits that bill. It's reasonably popular and easy to understand.

First, we'll add it to our package.json. We'll use yarn add again, but pass -D which means "this is a development dependency". That's important because when we get around to shipping our awesome Markdown previewer to production, we don't need our development dependencies to be part of that.

> yarn add -D jasmine
yarn add v1.3.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 3 new dependencies.
├─ exit@0.1.2
├─ jasmine-core@2.8.0
└─ jasmine@2.8.0
Done in 2.23s.

This will install Jasmine which includes the command-line app jasmine:

> $(yarn bin)/jasmine -h
Usage: jasmine [command] [options] [files]

Commands:
      init  initialize jasmine
  examples  install examples
   help,-h  show help
version,-v  show jasmine and jasmine-core versions

If no command is given, jasmine specs will be run


Options:
        --no-color  turn off color in spec output
         --filter=  filter specs to run only those that match the given string
         --helper=  load helper files that match the given string
--stop-on-failure=  [true|false] stop spec execution on expectation failure
         --config=  path to your optional jasmine.json

The given arguments take precedence over options in your jasmine.json
The path to your optional jasmine.json can also be configured by setting the JASMINE_CONFIG_PATH environment variable

Let's try out that init command.

> $(yarn bin)/jasmine init

OK, now we have tests?

> $(yarn bin)/jasmine
Started


No specs found
Finished in 0.002 seconds

Not quite. Let's create a simple no-op spec in spec/canary.spec.js:

describe("canary", function() {
  it("can run a test", function() {
    expect(true).toBe(true);
  });
});

Hopefully, Jasmine's syntax and API is clear, but the idea is that we use describe to block off a bunch of tests we'll write, and then it for each test. The idea is that these can be pieced together in some pidgen-like English that developers convince themselves is a specification. It's silly, but works.

And now we have a test:

> $(yarn bin)/jasmine
Started
.


1 spec, 0 failures
Finished in 0.006 seconds

OK, what has this to do with Webpack? Well, we need to actually locate our files to test.

Assuming we could do that, what is the test we'd like to write?

We're testing a function that returns a function, so our tests will be on the function that attachPreviewer returns to us. That function should:

Let's write that test first, before we figure out how to get a hold of attachPreviewer. In theory, Webpack should help us execute tests and not inform the way in which we write them. In theory.

In a Jasmine test, your top-level describe should be for the module being tested, and then you'd have one describe each for the routines you're going to test. That means our test file will have this form:

describe("markdownPreviewer", function() {
  describe("attachPreviewer", function() {
      // A bunch of `it` calls for each tests
      // (but we'll only need one right now )
  });
});

To write our test, we need to make some assumptions about some objects that will need to exist. In particular, we need to have some sort of object that exposes an innerHTML property that we can examine to make sure that we've rendered HTML to it. We also need another object that exposes a value property we can use to set the markdown being rendered. And, we need something to stand in for the global document, since, as you recall, we're passing that into attachPreviewer. Oh, and an event. Don't worry, we'll define all these.

Before we see where those come from, let's assume they exist so we can write our test.

First thing is to call attachPreviewer and get the function we're going to execute. Let's assume (I know, SO MANY assumptions…it'll work out, I promise) that we have markdownPreviewer available, which is our code.

var submitHandler = markdownPreviewer.attachPreviewer(document,
                                                      "source",
                                                      "preview");

document is assumed to exist, and we're also assuming that calls to document.getElementById("source") and document.getElementById("preview") both work and return objects we can manipulate.

Next, we want to manipulate source:

source.value = "This is _some markdown_";

Now, we can call submitHandler(event);

submitHandler(event);

The main point of our test is that we rendered markdown, so let's check for that:

expect(preview.innerHTML).toBe(
    "<p>This is <em>some markdown</em></p>");

We also want to make sure that preventDefault() was called on event, so let's hand-wave over that like so:

expect(event.preventDefaultCalled).toBe(true);

This means our entire test is:

describe("markdownPreviewer", function() {
  describe("attachPreviewer", function() {
    it("renders markdown to the preview element", function() {
      var submitHandler = markdownPreviewer.attachPreviewer(document,
                                                            "source",
                                                            "preview");
      source.value = "This is _some markdown_";

      submitHandler(event);

      expect(preview.innerHTML).toBe(
          "<p>This is <em>some markdown</em></p>");
      expect(event.preventDefaultCalled).toBe(true);
    });
  });
});

We've made a ton of assumptions about objects that exist and have behavior, so let's set that up now. We could use a mocking library, but I'm not willing to go to this level of the yak-shaving quite yet, so let's hand-jam these.

First, we'll make event:

var event = {
  preventDefaultCalled: false,
  preventDefault: function() { this.preventDefaultCalled = true; }
};

We know our code should call preventDefault, so we implement that to record that it was called in a way we can check in our test.

Next, we need our DOM elements, source, and preview:

var source = {
  value: ""
};

var preview = {
  innerHTML: ""
};

Pretty straightforward. Now, we need a mock document implementation that will return them. All this has to do is implement getElementById and we can hardcode its behavior:

var document = {
  getElementById: function(id) {
    if (id === "source") {
      return source;
    }
    else if (id === "preview") {
      return preview;
    }
    else {
      throw "Don't know how to get " + id;
    }
  }
}

Again, mocking might be better for a larger project, but this is enough to get us going and our test should work.

Except we still need access to our code.

This is JavaScript. We know this.

import markdownPreviewer from "../js/markdownPreviewer"

Seems like it should work. Here's the entire test file:

import markdownPreviewer from "../js/markdownPreviewer"

var event = {
  preventDefaultCalled: false,
  preventDefault: function() { this.preventDefaultCalled = true; }
};
var source = {
  value: "This is _some markdown_"
};
var preview = {
  innerHTML: ""
};

var document = {
  getElementById: function(id) {
    if (id === "source") {
      return source;
    }
    else if (id === "preview") {
      return preview;
    }
    else {
      throw "Don't know how to get " + id;
    }
  }
}

describe("markdownPreviewer", function() {
  describe("attachPreviewer", function() {
    it("renders markdown to the preview element", function() {
      var submitHandler = markdownPreviewer.attachPreviewer(document,
                                                            "source",
                                                            "preview");
      source.value = "This is _some markdown_";

      submitHandler(event);

      expect(preview.innerHTML).toBe(
        "<p>This is <em>some markdown</em></p>");
      expect(event.preventDefaultCalled).toBe(true);
    });
  });
});

Let's run our test and be excited!

> $(yarn bin)/jasmine
/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/spec/markdownPreview.spec.js:1
(function (exports, require, module, __filename, __dirname) { import markdownPreviewer from "../js/markdownPreviewer"
                                                              ^^^^^^

SyntaxError: Unexpected token import
    at createScript (vm.js:56:10)
    at Object.runInThisContext (vm.js:97:10)
    at Module._compile (module.js:542:28)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)
    at Module.require (module.js:497:17)
    at require (internal/module.js:20:19)
    at /Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/jasmine/lib/jasmine.js:93:5

Welp. Before I had walked through the previous two sections, I would've been confused and angry about this result. But, since we know what problem Webpack solves, and how it solves it in a minimal case, we shouldn't be surprised. Jasmine has no idea how to import, because Node doesn't support that syntax.

Since we are using Webpack for our actual code, it should be used to manage our test code as well. That way all of our code can be in a single verison of JavaScript that Webpack can compile.

Unfortunately, it isn't that easy. This won't work:

$(yarn bin)/webpack --entry=./spec/markdownPreviewer.spec.js --output-file=spec.js

But, even if it did, what do we do with spec.js? Jasmine's test runner (the jasmine command-line app) is expecting some JavaScript files that call describe and it. We could try doing something like

import describe from "jasmine"

But you'll get an error about fs which you've never heard of and didn't ask to use. Ugh.

In my experience, when things Completely Fail to Work at Even a Basic Level™, it tells me that I've made the wrong choice at some level higher than the code.

In this case, it's our choice of test runner. Note that I didn't say test framework, but test runner. Why are these even different things? What kind of test framework can't run tests?

In the JavaScript ecosystem: pretty much all of them.

Because each project is a chance to artisnally hand-craft a small batch tool chain, and because the language we're using can't even agree on something basic like how to modularize code, we end up with lots of tools that cannot interoperate together at all.

In this case, our desire to use import conflicts with Jasmine's inability to handle it.

What we need is a test runner that can both use Webpack to assemble our code, but also execute our tests using Jasmine.

That test runner that is Karma.

Karma: What Problem Does it Solve?

Karma executes tests, and is yet another tool where you learn about it by reading blog posts and pasting JSON boilerplate into your project and praying nothing goes wrong. As mentioned previously, that's not good enough.

We know that it has the ability to solve our problelm (mostly because I'm telling you in advance that this is the case), so let's see exactly how.

First, we'll install it

> yarn add -D karma
yarn add v1.3.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 87 new dependencies.
├─ accepts@1.3.3
├─ after@0.8.2
├─ array-slice@0.2.3
├─ arraybuffer.slice@0.0.6
├─ backo2@1.0.2
├─ base64-arraybuffer@0.1.5
├─ base64id@1.0.0
├─ better-assert@1.0.2
├─ blob@0.0.4
├─ bluebird@3.5.1
├─ body-parser@1.18.2
├─ braces@0.1.5
├─ bytes@3.0.0
├─ callsite@1.0.0
├─ colors@1.1.2
├─ combine-lists@1.0.1
├─ component-bind@1.0.0
├─ component-emitter@1.2.1
├─ component-inherit@0.0.3
├─ connect@3.6.5
├─ content-type@1.0.4
├─ cookie@0.3.1
├─ core-js@2.5.1
├─ custom-event@1.0.1
├─ debug@2.3.3
├─ depd@1.1.1
├─ di@0.0.1
├─ dom-serialize@2.2.1
├─ ee-first@1.1.1
├─ encodeurl@1.0.1
├─ engine.io-client@1.8.3
├─ engine.io-parser@1.3.2
├─ engine.io@1.8.3
├─ ent@2.2.0
├─ escape-html@1.0.3
├─ eventemitter3@1.2.0
├─ expand-braces@0.1.2
├─ expand-range@0.1.1
├─ finalhandler@1.0.6
├─ has-binary@0.1.7
├─ has-cors@1.1.0
├─ http-errors@1.6.2
├─ http-proxy@1.16.2
├─ iconv-lite@0.4.19
├─ is-number@0.1.1
├─ isarray@0.0.1
├─ isbinaryfile@3.0.2
├─ json3@3.3.2
├─ karma@1.7.1
├─ log4js@0.6.38
├─ lru-cache@2.2.4
├─ media-typer@0.3.0
├─ mime@1.4.1
├─ ms@0.7.2
├─ negotiator@0.6.1
├─ object-component@0.0.3
├─ on-finished@2.3.0
├─ optimist@0.6.1
├─ options@0.0.6
├─ parsejson@0.0.3
├─ parseqs@0.0.5
├─ parseuri@0.0.5
├─ parseurl@1.3.2
├─ qjobs@1.1.5
├─ qs@6.5.1
├─ range-parser@1.2.0
├─ raw-body@2.3.2
├─ requires-port@1.0.0
├─ setprototypeof@1.0.3
├─ socket.io-adapter@0.5.0
├─ socket.io-client@1.7.3
├─ socket.io-parser@2.3.1
├─ socket.io@1.7.3
├─ statuses@1.4.0
├─ tmp@0.0.31
├─ to-array@0.1.4
├─ type-is@1.6.15
├─ ultron@1.0.2
├─ unpipe@1.0.0
├─ useragent@2.2.1
├─ utils-merge@1.0.1
├─ void-elements@2.0.1
├─ wordwrap@0.0.3
├─ ws@1.1.2
├─ wtf-8@1.0.0
├─ xmlhttprequest-ssl@1.5.3
└─ yeast@0.1.2
Done in 5.52s.

Check that it's installed:

> $(yarn bin)/karma --version
Karma version: 1.7.1

Because Karma has no default test framework, we must install one for the framework we're using. In this case, that's Jasmine:

> yarn add -D karma-jasmine
yarn add v1.3.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
└─ karma-jasmine@1.1.0
Done in 2.35s.
warning " > karma-jasmine@1.1.0" has unmet peer dependency "jasmine-core@*".

Why not just instal karma-jasmine and have that pick up the dependency on karma? No idea. This is JavaScript, and we don't get nice things.

OK, now what? Karma's homepage currently states:

The main goal for Karma is to bring a productive testing environment to developers. The environment being one where they don't have to set up loads of configurations, but rather a place where developers can just write the code and get instant feedback from their tests.

It's OK to chuckle at their proclaimation that we don't need loads of configuration. Spoiler: we will.

It's common to use karma init to create a config file to start off with but this a) requires interactive input, b) places the file in the current directory, and c) creates a file with way more configuration than is technically needed. We don't want any of that, so create spec/karma.conf.js yourself. The bare minimum information you have to provide is:

There's no technical reason to have to provide any of this information, but JavaScript doesn't want to tell you where to put your files or what testing frameworks to use. It's cool with anything, so you do you (even though your job is to get things done and not evaluate a bunch of essentially equivalent testing frameworks).

The mimimal configuration would the following, which you should place into spec/karma.conf.js:

module.exports = function(config) {
  config.set({
    frameworks: ['jasmine'],
    files: [
      '**/*.spec.js'
    ],
    browsers: ['PhantomJS']
  })
}

Note that the location of our test files is relative to wherever the config file is. Since we put it in spec/, **/*.spec.js means "all the files in spec/ that end in .spec.js.

But I'm burying the lede. We had to do something with browsers. I know and I'm really sorry, but this is how it has to be.

On the one hand, this is terrible, because anything involving a browser will be incredibly slow and clunky. But, on the other hand, our code will only ever run in a browser, so as good developers that's technically the right place to test it.

PhantomJS is a browser that runs headlessly, meaning it's the only way to run our tests, with Karma, on the command line.

I know this sucks, and you should brace yourself for a terrible testing experience that's worse than all other ecosystems, but let's just give thanks that it's possible and that we don't have to pop up Firefox and deal with all that.

So…you'll need to install PhantomJS if you haven't. When you do that, you can test your intall thusly:

> phantomjs --version
2.1.1

You'll also need to install the PhantomJS launcher for Karma so it can execute PhantomJS. Because again, why would you want your testing tool telling you how to write tests? Clearly, what you want is a test runner that by default cannot run any tests in any browser. The JavaScript ecosystem is all about choices. Endless, endless choices.

> yarn add -D karma-phantomjs-launcher
yarn add v1.3.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 27 new dependencies.
├─ aws-sign2@0.7.0
├─ concat-stream@1.6.0
├─ es6-promise@4.1.1
├─ extract-zip@1.6.6
├─ fd-slicer@1.0.1
├─ form-data@2.3.1
├─ fs-extra@1.0.0
├─ har-schema@2.0.0
├─ har-validator@5.0.3
├─ hasha@2.2.0
├─ hoek@4.2.0
├─ http-signature@1.2.0
├─ jsonfile@2.4.0
├─ karma-phantomjs-launcher@1.0.4
├─ kew@0.7.0
├─ klaw@1.3.1
├─ pend@1.2.0
├─ performance-now@2.1.0
├─ phantomjs-prebuilt@2.1.16
├─ pinkie-promise@2.0.1
├─ pinkie@2.0.4
├─ progress@1.1.8
├─ request-progress@2.0.1
├─ request@2.83.0
├─ throttleit@1.0.0
├─ typedarray@0.0.6
└─ yauzl@2.4.1
Done in 4.06s.
warning " > karma-jasmine@1.1.0" has unmet peer dependency "jasmine-core@*".

With that done, we can now run our tests:

> $(yarn bin)/karma start spec/karma.conf.js  --single-run --no-color
07 11 2017 19:35:26.695:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
07 11 2017 19:35:26.699:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
07 11 2017 19:35:26.706:INFO [launcher]: Starting browser PhantomJS
07 11 2017 19:35:27.829:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket mYa2G5DTX1owtYh9AAAA with id 67175429
PhantomJS 2.1.1 (Mac OS X 0.0.0) ERROR
  SyntaxError: Use of reserved word 'import'
  at markdownPreview.spec.js:1

The --single-run means "actually run the tests and report the results". Without it, Karma sits there waiting for you to navigate to a web server it starts up that then runs the tests. Trust me, this is the best it gets for now.

You'll notice it failed with the same error as before. The difference here, is that Karma is sophisticated enough such that we can throw more JSON at it to fix the problem.

We need to have our tests use Webpack to bundle up our JavaScript so we can access it and run tests against it.

Karma's configuration file has an option called preprocessors that allows us to do stuff to our code before it runs the tests. There is such a preprocessor called karma-webpack that we can install and configure. First, we'll add it:

> yarn add -D karma-webpack
yarn add v1.3.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 5 new dependencies.
├─ amdefine@1.0.1
├─ karma-webpack@2.0.5
├─ loader-utils@0.2.17
├─ time-stamp@2.0.0
└─ webpack-dev-middleware@1.12.0
Done in 3.13s.
warning " > karma-jasmine@1.1.0" has unmet peer dependency "jasmine-core@*".

Now, we'll add it as a preprocessor and use require to bring in our existing Webpack config (not import. Sigh). Your entire spec/karma.conf.js will look like so:

module.exports = function(config) {
  config.set({
    frameworks: ['jasmine'],
    files: [
      '**/*.spec.js'
    ],
/* start new code */
    preprocessors: {
      '**/*.spec.js': [ 'webpack' ]
    },
    webpack: require('../webpack.config.js'),
/* end new code */
    browsers: ['PhantomJS']
  })
}

And wouldn't you know it, it works!

> $(yarn bin)/karma start spec/karma.conf.js  --single-run --no-color
webpack: wait until bundle finished: 
webpack: wait until bundle finished: 
Hash: cf86c6bd533984a3e44c
Version: webpack 3.8.1
Time: 54ms
                  Asset     Size  Chunks             Chunk Names
                   main  77.8 kB       0  [emitted]  main
markdownPreview.spec.js  78.6 kB       1  [emitted]  markdownPreview.spec.js
         canary.spec.js   2.6 kB       2  [emitted]  canary.spec.js
   [0] ./js/index.js 334 bytes {0} [built]
   [1] ./js/markdownPreviewer.js 381 bytes {0} {1} [built]
   [2] ./node_modules/markdown/lib/index.js 143 bytes {0} {1} [built]
   [3] ./node_modules/markdown/lib/markdown.js 51 kB {0} {1} [built]
   [4] ./node_modules/util/util.js 15.6 kB {0} {1} [built]
   [5] (webpack)/buildin/global.js 488 bytes {0} {1} [built]
   [6] ./node_modules/process/browser.js 5.42 kB {0} {1} [built]
   [7] ./node_modules/util/support/isBufferBrowser.js 203 bytes {0} {1} [built]
   [8] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {0} {1} [built]
   [9] ./spec/canary.spec.js 107 bytes {2} [built]
  [10] ./spec/markdownPreview.spec.js 1.09 kB {1} [built]
webpack: Compiled successfully.
07 11 2017 19:35:33.310:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
07 11 2017 19:35:33.312:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
07 11 2017 19:35:33.325:INFO [launcher]: Starting browser PhantomJS
07 11 2017 19:35:34.304:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket fyvNcOjeXu5FZElPAAAA with id 70907373
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 0 of 2 SUCCESS (0 secs / 0 secs)
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 1 of 2 SUCCESS (0 secs / 0.002 secs)
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 2 of 2 SUCCESS (0 secs / 0.004 secs)
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 2 of 2 SUCCESS (0.003 secs / 0.004 secs)

I'm not going to lie, I was not expecting this to work at all, especially with this fairly minimal amount of configuration. While it's not ergonomic or developer-friendly, it does work, and it was easy enough to figure out just by reading documentation. Take that Medium thinkpieces!

Before we move on, let's wrap this up into a script inside package.json, because typing all this out sucks.

{
  "scripts": {
    "webpack": "webpack --config webpack.config.js --display-error-details",
    "karma": "karma start spec/karma.conf.js --single-run --no-color"
  }
}

Now, we can run tests with yarn karma:

> yarn karma
yarn run v1.3.2
$ karma start spec/karma.conf.js --single-run --no-color
webpack: wait until bundle finished: 
webpack: wait until bundle finished: 
Hash: cf86c6bd533984a3e44c
Version: webpack 3.8.1
Time: 55ms
                  Asset     Size  Chunks             Chunk Names
                   main  77.8 kB       0  [emitted]  main
markdownPreview.spec.js  78.6 kB       1  [emitted]  markdownPreview.spec.js
         canary.spec.js   2.6 kB       2  [emitted]  canary.spec.js
   [0] ./js/index.js 334 bytes {0} [built]
   [1] ./js/markdownPreviewer.js 381 bytes {0} {1} [built]
   [2] ./node_modules/markdown/lib/index.js 143 bytes {0} {1} [built]
   [3] ./node_modules/markdown/lib/markdown.js 51 kB {0} {1} [built]
   [4] ./node_modules/util/util.js 15.6 kB {0} {1} [built]
   [5] (webpack)/buildin/global.js 488 bytes {0} {1} [built]
   [6] ./node_modules/process/browser.js 5.42 kB {0} {1} [built]
   [7] ./node_modules/util/support/isBufferBrowser.js 203 bytes {0} {1} [built]
   [8] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {0} {1} [built]
   [9] ./spec/canary.spec.js 107 bytes {2} [built]
  [10] ./spec/markdownPreview.spec.js 1.09 kB {1} [built]
webpack: Compiled successfully.
07 11 2017 19:35:35.924:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
07 11 2017 19:35:35.926:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
07 11 2017 19:35:35.929:INFO [launcher]: Starting browser PhantomJS
07 11 2017 19:35:36.937:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket tw7GqrblCVvRv6GbAAAA with id 20907658
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 0 of 2 SUCCESS (0 secs / 0 secs)
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 1 of 2 SUCCESS (0 secs / 0.002 secs)
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 2 of 2 SUCCESS (0 secs / 0.005 secs)
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 2 of 2 SUCCESS (0.004 secs / 0.005 secs)
Done in 2.27s.

The main thing to note here is the webpack: key in our Karma configuration file. Our use of require is essentially the same as if we copy and pasted our Webpack configuration into our Karma configuration. Re-using that config is good, because we don't have two things to keep up, but you can bet your ass that as we do more sophisticated things in Webpack, especially around production deployments, things are going to go sideways. We'll get to that.

In fact, we should push to production. The only thing that makes me more nervous than code without tests is code that isn't shipped to production.