diff --git a/lib/src/declarer.dart b/lib/src/declarer.dart
new file mode 100644
index 0000000000000000000000000000000000000000..9b3000645068d5cc7b9612708b2071041ceab8c7
--- /dev/null
+++ b/lib/src/declarer.dart
@@ -0,0 +1,94 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library unittest.declarer;
+
+import 'dart:collection';
+
+import 'group.dart';
+import 'invoker.dart';
+import 'test.dart';
+
+/// A class that manages the state of tests as they're declared.
+///
+/// This is in charge of tracking the current group, set-up, and tear-down
+/// functions. It produces a list of runnable [tests].
+class Declarer {
+  /// The current group.
+  var _group = new Group.root();
+
+  /// The list of tests that have been defined.
+  List<Test> get tests => new UnmodifiableListView<Test>(_tests);
+  final _tests = new List<Test>();
+
+  Declarer();
+
+  /// Defines a test case with the given description and body.
+  ///
+  /// The description will be added to the descriptions of any surrounding
+  /// [group]s.
+  void test(String description, body()) {
+    // TODO(nweiz): Once tests have begun running, throw an error if [test] is
+    // called.
+    var prefix = _group.description;
+    if (prefix != null) description = "$prefix $description";
+
+    var group = _group;
+    _tests.add(new LocalTest(description, () {
+      // TODO(nweiz): It might be useful to throw an error here if a test starts
+      // running while other tests from the same declarer are also running,
+      // since they might share closurized state.
+      return group.runSetUp().then((_) => body());
+    }, tearDown: group.runTearDown));
+  }
+
+  /// Creates a group of tests.
+  ///
+  /// A group's description is included in the descriptions of any tests or
+  /// sub-groups it contains. [setUp] and [tearDown] are also scoped to the
+  /// containing group.
+  void group(String description, void body()) {
+    var oldGroup = _group;
+    _group = new Group(oldGroup, description);
+    try {
+      body();
+    } finally {
+      _group = oldGroup;
+    }
+  }
+
+  /// Registers a function to be run before tests.
+  ///
+  /// This function will be called before each test is run. [callback] may be
+  /// asynchronous; if so, it must return a [Future].
+  ///
+  /// If this is called within a [group], it applies only to tests in that
+  /// group. [callback] will be run after any set-up callbacks in parent groups
+  /// or at the top level.
+  void setUp(callback()) {
+    if (_group.setUp != null) {
+      throw new StateError("setUp() may not be called multiple times for the "
+          "same group.");
+    }
+
+    _group.setUp = callback;
+  }
+
+  /// Registers a function to be run after tests.
+  ///
+  /// This function will be called after each test is run. [callback] may be
+  /// asynchronous; if so, it must return a [Future].
+  ///
+  /// If this is called within a [group], it applies only to tests in that
+  /// group. [callback] will be run before any tear-down callbacks in parent
+  /// groups or at the top level.
+  void tearDown(callback()) {
+    if (_group.tearDown != null) {
+      throw new StateError("tearDown() may not be called multiple times for "
+          "the same group.");
+    }
+
+    _group.tearDown = callback;
+  }
+}
diff --git a/lib/src/group.dart b/lib/src/group.dart
new file mode 100644
index 0000000000000000000000000000000000000000..e7de57c9d55cc4066982ea6fd0ffe3e75db4217f
--- /dev/null
+++ b/lib/src/group.dart
@@ -0,0 +1,76 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library unittest.group;
+
+import 'dart:async';
+
+import 'utils.dart';
+
+/// A group contains multiple tests and subgroups.
+///
+/// A group has a description that is prepended to that of all nested tests and
+/// subgroups. It also has [setUp] and [tearDown] functions which are scoped to
+/// the tests and groups it contains.
+class Group {
+  /// The parent group, or `null` if this is the root group.
+  final Group parent;
+
+  /// The description of the current test group, or `null` if this is the root
+  /// group.
+  final String _description;
+
+  /// The set-up function for this group, or `null`.
+  AsyncFunction setUp;
+
+  /// The tear-down function for this group, or `null`.
+  AsyncFunction tearDown;
+
+  /// Returns the description for this group, including the description of any
+  /// parent groups.
+  ///
+  /// If this is the root group, returns `null`.
+  String get description {
+    if (parent == null || parent.description == null) return _description;
+    return "${parent.description} $_description";
+  }
+
+  /// Creates a new root group.
+  ///
+  /// This is the implicit group that exists outside of any calls to `group()`.
+  Group.root()
+      : this(null, null);
+
+  Group(this.parent, this._description);
+
+  /// Run the set-up functions for this and any parent groups.
+  ///
+  /// If no set-up functions are declared, this returns a [Future] that
+  /// completes immediately.
+  Future runSetUp() {
+    if (parent != null) {
+      return parent.runSetUp().then((_) {
+        if (setUp != null) return setUp();
+      });
+    }
+
+    if (setUp != null) return new Future.sync(setUp);
+    return new Future.value();
+  }
+
+  /// Run the tear-up functions for this and any parent groups.
+  ///
+  /// If no set-up functions are declared, this returns a [Future] that
+  /// completes immediately.
+  Future runTearDown() {
+    if (parent != null) {
+      return new Future.sync(() {
+        if (tearDown != null) return tearDown();
+      }).then((_) => parent.runTearDown());
+    }
+
+    if (tearDown != null) return new Future.sync(tearDown);
+    return new Future.value();
+  }
+}
diff --git a/test/declarer_test.dart b/test/declarer_test.dart
new file mode 100644
index 0000000000000000000000000000000000000000..13211be29b80b3c086f79557d94bde161f7239bb
--- /dev/null
+++ b/test/declarer_test.dart
@@ -0,0 +1,326 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:unittest/src/declarer.dart';
+import 'package:unittest/src/suite.dart';
+import 'package:unittest/unittest.dart';
+
+Declarer _declarer;
+Suite _suite;
+
+void main() {
+  setUp(() {
+    _declarer = new Declarer();
+    _suite = new Suite("suite", []);
+  });
+
+  group(".test()", () {
+    test("declares a test with a description and body", () {
+      var bodyRun = false;
+      _declarer.test("description", () {
+        bodyRun = true;
+      });
+
+      expect(_declarer.tests, hasLength(1));
+      expect(_declarer.tests.single.name, equals("description"));
+
+      return _runTest(0).then(expectAsync((_) {
+        expect(bodyRun, isTrue);
+      }, max: 1));
+    });
+
+    test("declares multiple tests", () {
+      _declarer.test("description 1", () {});
+      _declarer.test("description 2", () {});
+      _declarer.test("description 3", () {});
+
+      expect(_declarer.tests, hasLength(3));
+      expect(_declarer.tests[0].name, equals("description 1"));
+      expect(_declarer.tests[1].name, equals("description 2"));
+      expect(_declarer.tests[2].name, equals("description 3"));
+    });
+  });
+
+  group(".setUp()", () {
+    test("is run before all tests", () {
+      var setUpRun = false;
+      _declarer.setUp(() => setUpRun = true);
+
+      _declarer.test("description 1", expectAsync(() {
+        expect(setUpRun, isTrue);
+        setUpRun = false;
+      }, max: 1));
+
+      _declarer.test("description 2", expectAsync(() {
+        expect(setUpRun, isTrue);
+        setUpRun = false;
+      }, max: 1));
+
+      return _runTest(0).then((_) => _runTest(1));
+    });
+
+    test("can return a Future", () {
+      var setUpRun = false;
+      _declarer.setUp(() {
+        return new Future(() => setUpRun = true);
+      });
+
+      _declarer.test("description", expectAsync(() {
+        expect(setUpRun, isTrue);
+      }, max: 1));
+
+      return _runTest(0);
+    });
+
+    test("can't be called multiple times", () {
+      _declarer.setUp(() {});
+      expect(() => _declarer.setUp(() {}), throwsStateError);
+    });
+  });
+
+  group(".tearDown()", () {
+    test("is run after all tests", () {
+      var tearDownRun;
+      _declarer.setUp(() => tearDownRun = false);
+      _declarer.tearDown(() => tearDownRun = true);
+
+      _declarer.test("description 1", expectAsync(() {
+        expect(tearDownRun, isFalse);
+      }, max: 1));
+
+      _declarer.test("description 2", expectAsync(() {
+        expect(tearDownRun, isFalse);
+      }, max: 1));
+
+      return _runTest(0).then((_) {
+        expect(tearDownRun, isTrue);
+        return _runTest(1);
+      }).then((_) => expect(tearDownRun, isTrue));
+    });
+
+    test("can return a Future", () {
+      var tearDownRun = false;
+      _declarer.tearDown(() {
+        return new Future(() => tearDownRun = true);
+      });
+
+      _declarer.test("description", expectAsync(() {
+        expect(tearDownRun, isFalse);
+      }, max: 1));
+
+      return _runTest(0).then((_) => expect(tearDownRun, isTrue));
+    });
+
+    test("can't be called multiple times", () {
+      _declarer.tearDown(() {});
+      expect(() => _declarer.tearDown(() {}), throwsStateError);
+    });
+  });
+
+  group("in a group,", () {
+    test("tests inherit the group's description", () {
+      _declarer.group("group", () {
+        _declarer.test("description", () {});
+      });
+
+      expect(_declarer.tests, hasLength(1));
+      expect(_declarer.tests.single.name, "group description");
+    });
+
+    group(".setUp()", () {
+      test("is scoped to the group", () {
+        var setUpRun = false;
+        _declarer.group("group", () {
+          _declarer.setUp(() => setUpRun = true);
+
+          _declarer.test("description 1", expectAsync(() {
+            expect(setUpRun, isTrue);
+            setUpRun = false;
+          }, max: 1));
+        });
+
+        _declarer.test("description 2", expectAsync(() {
+          expect(setUpRun, isFalse);
+          setUpRun = false;
+        }, max: 1));
+
+        return _runTest(0).then((_) => _runTest(1));
+      });
+
+      test("runs from the outside in", () {
+        var outerSetUpRun = false;
+        var middleSetUpRun = false;
+        var innerSetUpRun = false;
+        _declarer.setUp(expectAsync(() {
+          expect(middleSetUpRun, isFalse);
+          expect(innerSetUpRun, isFalse);
+          outerSetUpRun = true;
+        }, max: 1));
+
+        _declarer.group("middle", () {
+          _declarer.setUp(expectAsync(() {
+            expect(outerSetUpRun, isTrue);
+            expect(innerSetUpRun, isFalse);
+            middleSetUpRun = true;
+          }, max: 1));
+
+          _declarer.group("inner", () {
+            _declarer.setUp(expectAsync(() {
+              expect(outerSetUpRun, isTrue);
+              expect(middleSetUpRun, isTrue);
+              innerSetUpRun = true;
+            }, max: 1));
+
+            _declarer.test("description", expectAsync(() {
+              expect(outerSetUpRun, isTrue);
+              expect(middleSetUpRun, isTrue);
+              expect(innerSetUpRun, isTrue);
+            }, max: 1));
+          });
+        });
+
+        return _runTest(0);
+      });
+
+      test("handles Futures when chained", () {
+        var outerSetUpRun = false;
+        var innerSetUpRun = false;
+        _declarer.setUp(expectAsync(() {
+          expect(innerSetUpRun, isFalse);
+          return new Future(() => outerSetUpRun = true);
+        }, max: 1));
+
+        _declarer.group("inner", () {
+          _declarer.setUp(expectAsync(() {
+            expect(outerSetUpRun, isTrue);
+            return new Future(() => innerSetUpRun = true);
+          }, max: 1));
+
+          _declarer.test("description", expectAsync(() {
+            expect(outerSetUpRun, isTrue);
+            expect(innerSetUpRun, isTrue);
+          }, max: 1));
+        });
+
+        return _runTest(0);
+      });
+
+      test("can't be called multiple times", () {
+        _declarer.group("group", () {
+          _declarer.setUp(() {});
+          expect(() => _declarer.setUp(() {}), throwsStateError);
+        });
+      });
+    });
+
+    group(".tearDown()", () {
+      test("is scoped to the group", () {
+        var tearDownRun;
+        _declarer.setUp(() => tearDownRun = false);
+
+        _declarer.group("group", () {
+          _declarer.tearDown(() => tearDownRun = true);
+
+          _declarer.test("description 1", expectAsync(() {
+            expect(tearDownRun, isFalse);
+          }, max: 1));
+        });
+
+        _declarer.test("description 2", expectAsync(() {
+          expect(tearDownRun, isFalse);
+        }, max: 1));
+
+        return _runTest(0).then((_) {
+          expect(tearDownRun, isTrue);
+          return _runTest(1);
+        }).then((_) => expect(tearDownRun, isFalse));
+      });
+
+      test("runs from the inside out", () {
+        var innerTearDownRun = false;
+        var middleTearDownRun = false;
+        var outerTearDownRun = false;
+        _declarer.tearDown(expectAsync(() {
+          expect(innerTearDownRun, isTrue);
+          expect(middleTearDownRun, isTrue);
+          outerTearDownRun = true;
+        }, max: 1));
+
+        _declarer.group("middle", () {
+          _declarer.tearDown(expectAsync(() {
+            expect(innerTearDownRun, isTrue);
+            expect(outerTearDownRun, isFalse);
+            middleTearDownRun = true;
+          }, max: 1));
+
+          _declarer.group("inner", () {
+            _declarer.tearDown(expectAsync(() {
+              expect(outerTearDownRun, isFalse);
+              expect(middleTearDownRun, isFalse);
+              innerTearDownRun = true;
+            }, max: 1));
+
+            _declarer.test("description", expectAsync(() {
+              expect(outerTearDownRun, isFalse);
+              expect(middleTearDownRun, isFalse);
+              expect(innerTearDownRun, isFalse);
+            }, max: 1));
+          });
+        });
+
+        return _runTest(0).then((_) {
+          expect(innerTearDownRun, isTrue);
+          expect(middleTearDownRun, isTrue);
+          expect(outerTearDownRun, isTrue);
+        });
+      });
+
+      test("handles Futures when chained", () {
+        var outerTearDownRun = false;
+        var innerTearDownRun = false;
+        _declarer.tearDown(expectAsync(() {
+          expect(innerTearDownRun, isTrue);
+          return new Future(() => outerTearDownRun = true);
+        }, max: 1));
+
+        _declarer.group("inner", () {
+          _declarer.tearDown(expectAsync(() {
+            expect(outerTearDownRun, isFalse);
+            return new Future(() => innerTearDownRun = true);
+          }, max: 1));
+
+          _declarer.test("description", expectAsync(() {
+            expect(outerTearDownRun, isFalse);
+            expect(innerTearDownRun, isFalse);
+          }, max: 1));
+        });
+
+        return _runTest(0).then((_) {
+          expect(innerTearDownRun, isTrue);
+          expect(outerTearDownRun, isTrue);
+        });
+      });
+
+      test("can't be called multiple times", () {
+        _declarer.group("group", () {
+          _declarer.tearDown(() {});
+          expect(() => _declarer.tearDown(() {}), throwsStateError);
+        });
+      });
+    });
+  });
+}
+
+/// Runs the test at [index] defined on [_declarer].
+///
+/// This automatically sets up an `onError` listener to ensure that the test
+/// doesn't throw any invisible exceptions.
+Future _runTest(int index) {
+  var liveTest = _declarer.tests[index].load(_suite);
+  liveTest.onError.listen(expectAsync((_) {},
+      count: 0, reason: "No errors expected for test #$index."));
+  return liveTest.run();
+}