TestTask = function () {
  let self = this;
  let args = Array.prototype.slice.call(arguments);
  let name = args.shift();
  let definition = args.pop();
  let prereqs = args.pop() || [];

  /**
    @name jake.TestTask#testNam
    @public
    @type {String}
    @description The name of the namespace to place the tests in, and
    the top-level task for running tests. Defaults to "test"
   */
  this.testName = 'test';

  /**
    @name jake.TestTask#testFiles
    @public
    @type {jake.FileList}
    @description The list of files containing tests to load
   */
  this.testFiles = new jake.FileList();

  /**
    @name jake.TestTask#showDescription
    @public
    @type {Boolean}
    @description Show the created task when doing Jake -T
   */
  this.showDescription = true;

  /*
    @name jake.TestTask#totalTests
    @public
    @type {Number}
    @description The total number of tests to run
  */
  this.totalTests = 0;

  /*
    @name jake.TestTask#executedTests
    @public
    @type {Number}
    @description The number of tests successfully run
  */
  this.executedTests = 0;

  if (typeof definition == 'function') {
    definition.call(this);
  }

  if (this.showDescription) {
    desc('Run the tests for ' + name);
  }

  task(this.testName, prereqs, {async: true}, function () {
    let t = jake.Task[this.fullName + ':run'];
    t.on('complete', function () {
      complete();
    });
    // Pass args to the namespaced test
    t.invoke.apply(t, arguments);
  });

  namespace(self.testName, function () {

    let runTask = task('run', {async: true}, function (pat) {
      let re;
      let testFiles;

      // Don't nest; make a top-level namespace. Don't want
      // re-calling from inside to nest infinitely
      jake.currentNamespace = jake.defaultNamespace;

      re = new RegExp(pat);
      // Get test files that match the passed-in pattern
      testFiles = self.testFiles.toArray()
        .filter(function (f) {
          return (re).test(f);
        }) // Don't load the same file multiple times -- should this be in FileList?
        .reduce(function (p, c) {
          if (p.indexOf(c) < 0) {
            p.push(c);
          }
          return p;
        }, []);

      // Create a namespace for all the testing tasks to live in
      namespace(self.testName + 'Exec', function () {
        // Each test will be a prereq for the dummy top-level task
        let prereqs = [];
        // Continuation to pass to the async tests, wrapping `continune`
        let next = function () {
          complete();
        };
        // Create the task for this test-function
        let createTask = function (name, action) {
          // If the test-function is defined with a continuation
          // param, flag the task as async
          let t;
          let isAsync = !!action.length;

          // Define the actual namespaced task with the name, the
          // wrapped action, and the correc async-flag
          t = task(name, createAction(name, action), {
            async: isAsync
          });
          t.once('complete', function () {
            self.executedTests++;
          });
          t._internal = true;
          return t;
        };
        // Used as the action for the defined task for each test.
        let createAction = function (n, a) {
          // A wrapped function that passes in the `next` function
          // for any tasks that run asynchronously
          return function () {
            let cb;
            if (a.length) {
              cb = next;
            }
            if (!(n == 'before' || n == 'after' ||
                    /_beforeEach$/.test(n) || /_afterEach$/.test(n))) {
              jake.logger.log(n);
            }
            // 'this' will be the task when action is run
            return a.call(this, cb);
          };
        };
          // Dummy top-level task for everything to be prereqs for
        let topLevel;

        // Pull in each test-file, and iterate over any exported
        // test-functions. Register each test-function as a prereq task
        testFiles.forEach(function (file) {
          let exp = require(path.join(currDir, file));

          // Create a namespace for each filename, so test-name collisions
          // won't be a problem
          namespace(file, function () {
            let testPrefix = self.testName + 'Exec:' + file + ':';
            let testName;
            // Dummy task for displaying file banner
            testName = '*** Running ' + file + ' ***';
            prereqs.push(testPrefix + testName);
            createTask(testName, function () {});

            // 'before' setup
            if (typeof exp.before == 'function') {
              prereqs.push(testPrefix + 'before');
              // Create the task
              createTask('before', exp.before);
            }

            // Walk each exported function, and create a task for each
            for (let p in exp) {
              if (p == 'before' || p == 'after' ||
                  p == 'beforeEach' || p == 'afterEach') {
                continue;
              }

              if (typeof exp.beforeEach == 'function') {
                prereqs.push(testPrefix + p + '_beforeEach');
                // Create the task
                createTask(p + '_beforeEach', exp.beforeEach);
              }

              // Add the namespace:name of this test to the list of prereqs
              // for the dummy top-level task
              prereqs.push(testPrefix + p);
              // Create the task
              createTask(p, exp[p]);

              if (typeof exp.afterEach == 'function') {
                prereqs.push(testPrefix + p + '_afterEach');
                // Create the task
                createTask(p + '_afterEach', exp.afterEach);
              }
            }

            // 'after' teardown
            if (typeof exp.after == 'function') {
              prereqs.push(testPrefix + 'after');
              // Create the task
              let afterTask = createTask('after', exp.after);
              afterTask._internal = true;
            }

          });
        });

        self.totalTests = prereqs.length;
        process.on('exit', function () {
          // Throw in the case where the process exits without
          // finishing tests, but no error was thrown
          if (!jake.errorCode && (self.totalTests > self.executedTests)) {
            throw new Error('Process exited without all tests completing.');
          }
        });

        // Create the dummy top-level task. When calling a task internally
        // with `invoke` that is async (or has async prereqs), have to listen
        // for the 'complete' event to know when it's done
        topLevel = task('__top__', prereqs);
        topLevel._internal = true;
        topLevel.addListener('complete', function () {
          jake.logger.log('All tests ran successfully');
          complete();
        });

        topLevel.invoke(); // Do the thing!
      });

    });
    runTask._internal = true;

  });


}
@name jake.TestTask
  @constructor
  @description Instantiating a TestTask creates a number of Jake
  Tasks that make running tests for your software easy.

  @param {String} name The name of the project
  @param {Function} definition Defines the list of files containing the tests,
  and the name of the namespace/task for running them. Will be executed on the
  instantiated TestTask (i.e., 'this', will be the TestTask instance), to set
  the various instance-propertiess.

  @example
  let t = new jake.TestTask('bij-js', function () {
    this.testName = 'testSpecial';
    this.testFiles.include('test/**');
  });